/*
This file is part of maven-bugzilla-plugin.

    maven-bugzilla-plugin is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    maven-bugzilla-plugin is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with maven-bugzilla-plugin.  If not, see <http://www.gnu.org/licenses/>.
 */
package nl.semlab.maven.plugin.bugzilla.model;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.protocol.Protocol;
import org.apache.maven.plugin.logging.Log;
import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.client.XmlRpcClient;
import org.apache.xmlrpc.client.XmlRpcClientConfigImpl;
import org.apache.xmlrpc.client.XmlRpcCommonsTransportFactory;

import nl.semlab.maven.plugin.bugzilla.exceptions.BugzillaException;
import nl.semlab.maven.plugin.bugzilla.util.EasySSLProtocolSocketFactory;

/**
 * Communicate with Bugzilla via a Webservice (or directly via HTTP).
 * <p />
 * Project maven-bugzilla-plugin<br />
 * BugzillaWebservice.java created 20 Apr 2009
 * <p />
 * Copyright &copy 2009 SemLab
 * 
 * @author <a href="mailto:borsje@semlab.nl">J.A. Borsje</a>
 * @version $Revision: 1.3 $, $Date: 2009-08-31 14:18:56 $
 */
public class BugzillaWebservice
{
   private final Log log;
   private final String server;
   private final String user;
   private final String pass;
   private final XmlRpcClient rpcClient;

   /**
    * Create the Bugzilla webservice object.
    * 
    * @param log
    *           A reference to the Maven logger.
    * @param server
    *           The Bugzilla server address.
    * @param user
    *           The user name for Bugzilla.
    * @param pass
    *           The password for Bugizlla.
    * @throws BugzillaException
    *            Thrown when the URL of the server could not be created.
    */
   public BugzillaWebservice(Log log, String server, String user, String pass) throws BugzillaException
   {
      this.log = log;
      this.server = server;
      this.user = user;
      this.pass = pass;
      URL url = null;
      try
      {
         url = new URL(this.server + "xmlrpc.cgi");
      }
      catch (MalformedURLException ex)
      {
         throw new BugzillaException("The server url could not be created, using: " + this.server, ex);
      }

      rpcClient = new XmlRpcClient();
      Protocol.registerProtocol("https", new Protocol("https", new EasySSLProtocolSocketFactory(), 443));
      XmlRpcCommonsTransportFactory factory = new XmlRpcCommonsTransportFactory(rpcClient);
      factory.setHttpClient(new HttpClient());
      rpcClient.setTransportFactory(factory);
      XmlRpcClientConfigImpl config = new XmlRpcClientConfigImpl();
      config.setServerURL(url);
      rpcClient.setConfig(config);
   }

   /**
    * Login to bugzilla.
    * 
    * @return The returned value from Bugzilla.
    * @throws BugzillaException
    *            Thrown if the login to Bugzilla failed.
    */
   public Object login() throws BugzillaException
   {
      log.debug("Logging in using " + user + ":" + pass);
      Map<String, String> loginMap = new HashMap<String, String>();
      loginMap.put("login", user);
      loginMap.put("password", pass);
      loginMap.put("rememberlogin", "Bugzilla_remember");
      Object retval = null;
      try
      {
         retval = rpcClient.execute("User.login", new Object[]
         {loginMap});
      }
      catch (XmlRpcException ex)
      {
         throw new BugzillaException("The login to Bugzilla failed.", ex);
      }
      return retval;
   }

   /**
    * Add a version with the current version number to Bugzilla.
    * 
    * @param product
    *           The product to which to add the version.
    * @param version
    *           The new version.
    * @throws BugzillaException
    *            Thrown when the version could not be added.
    */
   public void addVersion(String product, String version) throws BugzillaException
   {
      log.info("Adding version " + version + " to product " + product);
      HttpClient client = new HttpClient();
      HttpMethod method = null;
      String url = server + "editversions.cgi";
      log.debug("url: " + url);
      method = new GetMethod(url);
      method.setFollowRedirects(true);
      try
      {
         method.setQueryString("version=" + URLEncoder.encode(version, "UTF-8") + "&action=new&product="
               + URLEncoder.encode(product, "UTF-8") + "&" + this.getHTTPAuthentificationPart());
         log.debug("Executing: " + method.getQueryString());
         client.executeMethod(method);
         if (method.getStatusCode() != 200)
         {
            String message = "HTTP error code: '" + method.getStatusLine() + "' with response body: "
                  + method.getResponseBodyAsString();
            throw new BugzillaException(message);
         }

         String reply = this.convertStreamToString(method.getResponseBodyAsStream());
         String formattedReply = reply.replaceAll(" ", "").replaceAll("\n", "");
         String searchString = "<input type=\"hidden\" name=\"token\" value=\"";
         String formattedSearchString = searchString.replaceAll(" ", "").replaceAll("\n", "");
         int beginIndex = formattedReply.indexOf(formattedSearchString) + formattedSearchString.length();
         int endIndex = formattedReply.indexOf("\">", beginIndex);
         String token = formattedReply.substring(beginIndex, endIndex);
         method.setQueryString(method.getQueryString() + "&token=" + URLEncoder.encode(token, "UTF-8"));
         log.debug("Executing: " + method.getQueryString());
         client.executeMethod(method);
         if (method.getStatusCode() != 200)
         {
            String message = "HTTP error code: '" + method.getStatusLine() + "' with response body: "
                  + method.getResponseBodyAsString();
            throw new BugzillaException(message);
         }
      }
      catch (HttpException ex)
      {
         throw new BugzillaException("The HTTP method could not be executed.", ex);
      }
      catch (IOException ex)
      {
         throw new BugzillaException("An I/O error occurred.", ex);
      }
      finally
      {
         method.releaseConnection();
      }
   }

   /**
    * Get a product from Bugzilla.
    * 
    * @param product
    *           The id of the product
    * @return The Bugzilla product.
    * @throws BugzillaException
    *            Thrown if the product could not be retrieved.
    */
   @SuppressWarnings("unchecked")
   public BugzillaProduct getProduct(String product) throws BugzillaException
   {
      BugzillaProduct retval = null;
      try
      {
         Map<String, String[]> argument = new HashMap<String, String[]>();
         argument.put("ids", new String[]
         {"26"});
         HashMap<String, Object[]> getResult = (HashMap<String, Object[]>) rpcClient.execute("Product.get",
               new Object[]
               {argument});
         Object[] products = getResult.get("products");
         // LOG.debug("Found products #" + products.length + ": " + products +
         // " for product id: " + product);
         // for (Object bugzillaProduct : products)
         // {
         // LOG.debug("" + bugzillaProduct);
         // }
         if (products.length == 1)
         {
            retval = new BugzillaProduct((Map<String, Object>) products[0]);
         }
         else
         {
            throw new BugzillaException("The project could not be found.");
         }
      }
      catch (XmlRpcException ex)
      {
         throw new BugzillaException("The project could not be retrieved.", ex);
      }
      return retval;
   }

   /**
    * Add a bug to a product in Bugzilla.
    * 
    * @param bugReport
    *           The bug to add.
    * @return The id of the new bug.
    * @throws BugzillaException
    *            Thrown when the bug could not be added.
    */
   @SuppressWarnings("unchecked")
   public Integer addBug(Map<String, String> bugReport) throws BugzillaException
   {
      Integer retval = null;
      try
      {
         HashMap<String, Integer> createResult = (HashMap<String, Integer>) rpcClient.execute("Bug.create",
               new Object[]
               {bugReport});
         log.debug("bug creation result: " + createResult);
         retval = createResult.get("id");
      }
      catch (XmlRpcException ex)
      {
         throw new BugzillaException("The bug could not be created.", ex);
      }
      return retval;
   }

   /**
    * Check if there already is an open bug with the given alias.
    * 
    * @param alias
    *           The alias used to check for duplicates.
    * @return True if there is an open bug with the same alias, false otherwise.
    */
   public boolean hasDuplicate(String alias)
   {
      boolean retval = false;
      // TODO: Do duplicate detection once we are able to search Bugzilla on a
      // custom field.
      // Map<String, String[]> argument = new HashMap<String, String[]>();
      // argument.put("ids", new String[]{this.getAlias(bug)});
      // try
      // {
      // Map<String, Object[]> duplicatesResult = (HashMap<String, Object[]>)
      // super.rpcClient.execute("Bug.get",
      // new Object[]{argument});
      // super.getLog().debug("bug duplicate check result: " +
      // duplicatesResult);
      // Object[] duplicates = duplicatesResult.get("bugs");
      // int handled = 0;
      // // Check if the duplicates are marked ass resolved, if this is the case
      // the duplicate does not count as
      // // duplicate.
      // for (Object duplicate : duplicates)
      // {
      // this.getLog().debug("duplicate: " + duplicate.toString());
      // Map<String, Object> duplicateBug = (Map<String, Object>) duplicate;
      // String status = ((Map<String, Object>)
      // duplicateBug.get("internals")).get("bug_status").toString();
      // this.getLog().debug("status: " + status);
      // if (HANDLED_BUG_STATES.contains(status))
      // {
      // this.getLog().debug("bug is said to be handled.");
      // handled++;
      // }
      // }
      // retval = (duplicates.length - handled) > 0;
      // }
      // catch (XmlRpcException ex)
      // {
      // // Catch the XmlRpcException, it might be thrown because a bug with the
      // given alias does not exist. If this is
      // // the case we do nothing, if this is not the case another error
      // occured and we throw the exception.
      // String msg = ex.getMessage();
      // this.getLog().debug(msg);
      // if (!msg.contains("is not a valid bug number") &&
      // !msg.contains("nor an alias to a bug."))
      // {
      // throw ex;
      // }
      // }
      return retval;
   }

   /**
    * Add a milestone with the current version number to Bugzilla.
    * 
    * @param product
    *           The product to which to add the milestone.
    * @param milestone
    *           The new milestone.
    * @throws BugzillaException
    *            Thrown when the milestone could not be added.
    */
   public void addMilestone(String product, String milestone) throws BugzillaException
   {
      log.info("Adding milestone " + milestone + " to product " + product);
      HttpClient client = new HttpClient();
      HttpMethod method = null;
      String url = server + "editmilestones.cgi";
      log.debug("url: " + url);
      method = new GetMethod(url);
      method.setFollowRedirects(true);
      try
      {
         method.setQueryString("milestone=" + URLEncoder.encode(milestone, "UTF-8") + "&action=new&product="
               + URLEncoder.encode(product, "UTF-8") + "&" + this.getHTTPAuthentificationPart());
         log.debug("Executing: " + method.getQueryString());
         client.executeMethod(method);
         if (method.getStatusCode() != 200)
         {
            String message = "HTTP error code: '" + method.getStatusLine() + "' with response body: "
                  + method.getResponseBodyAsString();
            throw new BugzillaException(message);
         }

         String reply = this.convertStreamToString(method.getResponseBodyAsStream());
         String formattedReply = reply.replaceAll(" ", "").replaceAll("\n", "");
         String searchString = "<input type=\"hidden\" name=\"token\" value=\"";
         String formattedSearchString = searchString.replaceAll(" ", "").replaceAll("\n", "");
         int beginIndex = formattedReply.indexOf(formattedSearchString) + formattedSearchString.length();
         int endIndex = formattedReply.indexOf("\">", beginIndex);
         String token = formattedReply.substring(beginIndex, endIndex);
         method.setQueryString(method.getQueryString() + "&token=" + URLEncoder.encode(token, "UTF-8"));
         log.debug("Executing: " + method.getQueryString());
         client.executeMethod(method);
         if (method.getStatusCode() != 200)
         {
            String message = "HTTP error code: '" + method.getStatusLine() + "' with response body: "
                  + method.getResponseBodyAsString();
            throw new BugzillaException(message);
         }
      }
      catch (HttpException ex)
      {
         throw new BugzillaException("The HTTP method could not be executed.", ex);
      }
      catch (IOException ex)
      {
         throw new BugzillaException("An I/O error occurred.", ex);
      }
      finally
      {
         method.releaseConnection();
      }
   }

   /**
    * @return The auhtentification part of the querystring for HTTP using
    *         configured Bugzilla login and password.
    * @throws BugzillaException
    *            Thrown when the username or password could not be encoded.
    */
   private String getHTTPAuthentificationPart() throws BugzillaException
   {
      // Do not use encoding here, because if the "@" in the username is encoded
      // logging in fails.
      // try
      // {
      // return "Bugzilla_login=" + URLEncoder.encode(this.user, "UTF-8") +
      // "&Bugzilla_password="
      // + URLEncoder.encode(this.pass, "UTF-8");
      return "Bugzilla_login=" + user + "&Bugzilla_password=" + pass;
      // }
      // catch (UnsupportedEncodingException e)
      // {
      // String message =
      // "Unsupported encoding error while URL encoding (UTF-8)"
      // + " configured Bugzilla user login and password: " + e.getMessage();
      // throw new BugzillaException(message, e);
      // }
   }

   /**
    * To convert the InputStream to String we use the BufferedReader.readLine()
    * method. We iterate until the BufferedReader return null which means
    * there's no more data to read. Each line will appended to a StringBuilder
    * and returned as String.
    * 
    * @param is
    *           The input stream to convert.
    * @return The String in the input stream.
    * @throws IOException
    *            Thrown if the input stream could not be converted.
    */
   private String convertStreamToString(InputStream is) throws IOException
   {
      BufferedReader reader = new BufferedReader(new InputStreamReader(is));
      StringBuilder sb = new StringBuilder();
      String line = null;
      try
      {
         while ((line = reader.readLine()) != null)
         {
            sb.append(line + "\n");
         }
      }
      finally
      {
         is.close();
      }
      return sb.toString();
   }
}